package com.gkzf.ai.module.crm.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
public class HtmlTempkateCommonMethod {

    // 定义常量，使代码更易读和维护
    private static final int CURRENT_YEAR = 2025;
    private static final int YEAR_OFFSET = 50;
    private static final int LOWER_BOUND = CURRENT_YEAR - YEAR_OFFSET; // 1975
    private static final int UPPER_BOUND = CURRENT_YEAR + YEAR_OFFSET; // 2075

    /**
     * Checks if a given year (as an integer) is outside the range [1975, 2075].
     *
     * @param year The year to check.
     * @return true if the year is less than 1975 or greater than 2075; otherwise false.
     */
    private static boolean isYearOutsideFiftyYearRange(int year) {
        // 判断是否小于下限 或 大于上限
        return year < LOWER_BOUND || year > UPPER_BOUND;
    }

    /**
     * 检查给定年份（作为字符串）是否在[1975,2075]范围之外。
     * 处理输入字符串的潜在解析错误。
     *
     * @param yearString 要检查的年份，以字符串形式表示。可以包含前导/尾随空格。
     * @true 返回如果字符串是一个有效的整数，并且年份在[1975,2075]范围之外； 否则为false（包括如果字符串不是有效整数）
     */
    public static boolean isYearOutsideFiftyYearRange(String yearString) {
        // 检查输入字符串是否有效
        if (Objects.isNull(yearString) || yearString.trim().isEmpty()) {
            return false; // 无效输入，返回 false
        }

        try {
            // 尝试将字符串解析为整数，并去除首尾空白
            int year = Integer.parseInt(yearString.trim());
            // 调用判断整数年份的方法
            return isYearOutsideFiftyYearRange(year);
        } catch (NumberFormatException e) {
            // 如果字符串不是有效的整数格式
            // 根据需求决定：如果无效格式也算“不属于”范围，这里可以返回 true
            // 如果只判断有效的数字年份，则返回 false
            // 这里选择返回 false，表示它不是一个可以在数值上进行范围判断的有效年份
            System.err.println("Warning: Invalid year string format received: '" + yearString + "'"); // 可选：打印警告
            return false;
        }
    }

    // 定义常量，使代码更易读和维护
    private static final int MIN_MONTH_COUNT = 10; // 至少满足10次就算月份
    private static final int MIN_MONTH_VALUE = 1;  // 月份的最小值
    private static final int MAX_MONTH_VALUE = 12; // 月份的最大值

    /**
     * 检查一个字符串数字集合中，是否有至少 MIN_MONTH_COUNT 个值能被解析为整数
     * 并且这些整数在 MIN_MONTH_VALUE 到 MAX_MONTH_VALUE 之间（包含边界）。
     *
     * @param numberStrings 要检查的字符串集合。
     * @return 如果集合中至少有 MIN_MONTH_COUNT 个数字在 1 到 12 之间，则返回 true；
     * 否则返回 false。如果输入的集合为 null 或为空，也返回 false。
     */
    public static boolean isCollectionLikelyMonths(Collection<String> numberStrings) {
        // 处理无效输入：集合为 null 或为空
        if (Objects.isNull(numberStrings) || numberStrings.isEmpty()) {
            return false;
        }

        int monthLikeCount = 0; // 计数器，记录在 1-12 范围内的数字个数

        // 遍历集合中的每个字符串
        for (String numberStr : numberStrings) {
            // 忽略 null 或只包含空白的字符串
            if (Objects.isNull(numberStr) || numberStr.trim().isEmpty()) {
                continue; // 跳过本次循环，处理下一个元素
            }

            try {
                // 尝试将字符串转换为整数，并去除首尾空白
                int number = Integer.parseInt(numberStr.trim());

                // 判断数字是否在 1 到 12 之间（包含边界）
                if (number >= MIN_MONTH_VALUE && number <= MAX_MONTH_VALUE) {
                    monthLikeCount++; // 在范围内，计数器加一
                }

            } catch (NumberFormatException e) {
                // 如果字符串不能被解析为有效的整数格式，忽略该元素
                // System.err.println("警告：无法将 '" + numberStr + "' 解析为整数。"); // 可选：打印警告
            }

            // 优化：如果已经达到所需的最小计数（10次），可以提前结束循环并返回 true
            if (monthLikeCount >= MIN_MONTH_COUNT) {
                return true;
            }
        }

        // 遍历完所有元素后，检查计数器是否达到阈值（10次）
        return monthLikeCount >= MIN_MONTH_COUNT;
    }


    private static final String SIMPLE_REPAYMENT_PATTERN = "年.*?月.*?还款记录";
    private static final Pattern PATTERN = Pattern.compile(SIMPLE_REPAYMENT_PATTERN);


    /**
     * 判断输入的字符串是否包含 "某某年某某月还款记录" 字样 (中间无空格)。
     * 使用 \d{2} 匹配两位数的月份。
     *
     * @param inputString 要检查的字符串。
     * @return 如果包含指定的模式，则返回 true；否则返回 false。
     */
    public static boolean containsYearMonthRepaymentRecordPattern(String inputString) {
        if (inputString == null) {
            return false;
        }
        // 创建 Matcher 对象
        Matcher matcher = PATTERN.matcher(inputString);

        // 使用 find() 方法检查字符串中是否存在匹配的子串
        return matcher.find(); // find() 查找任何匹配的子串
    }




    /**
     * 查询 textPatterns 在 document 中的匹配元素
     *
     * @param document
     * @param textPatterns
     * @return
     */
    public static Element findLoanAccountHeading(Document document, List<String> textPatterns) {
    /*
      查找贷款信息相关的开始段落标题 (非循环贷, 循环贷一, 循环贷二)
      根据预定义的优先级顺序查找，找到第一个匹配的元素即返回。
      优先级顺序是：
      1. 文本模式 ".*一.*非循环贷账户.*" 在 h1, h2, h3 中查找
      2. 文本模式 ".*二.*循环贷账户一.*" 在 h1, h2, h3 中查找
      3. 文本模式 ".*三.*循环贷账户二.*" 在 h1, h2, h3 中查找
     */

        if (document == null) {
            return null; // 处理document为null的情况
        }

        // 定义要查找的标签，按优先级排序
        List<String> headingTags = Arrays.asList("h1", "h2", "h3");

        // 遍历文本模式（高优先级在外层）
        for (String pattern : textPatterns) {
            // 遍历标签（次高优先级在内层）
            for (String tag : headingTags) {
                // 构建选择器
                String selector = tag + ":matches(" + pattern + ")";

                // 尝试查找第一个匹配的元素
                Element loanAccountIndex = document.selectFirst(selector);

                // 如果找到了，说明这是当前优先级下第一个匹配的元素，立即返回
                if (Objects.nonNull(loanAccountIndex)) {
                    return loanAccountIndex;
                }
            }
        }

        // 如果所有模式和标签组合都未找到匹配项，返回 null
        return null;
    }

    public static String matchKey(Map<String, String> accountDetails, String key) {
        if (accountDetails.containsKey(key)) {
            return key;
        }
        for (Map.Entry<String, String> entry : accountDetails.entrySet()) {
            if (!Objects.equals(entry.getValue(), "1")) {
                // 将 value 字符串编译为正则表达式模式。
                // 考虑将模式预编译到另一个 Map<String, Pattern> 中。
                Pattern pattern = Pattern.compile(entry.getValue());
                Matcher matcher = pattern.matcher(key);
                if (matcher.find()) {
                    return entry.getKey();
                }
            }
        }
        return null;
    }

    /**
     * map 转换list
     *
     * @param map
     * @return
     */
    public static List<String> mapConvertList(Map<String, String> map) {
        List<String> mapList = new ArrayList<>();
        for (Map.Entry<String, String> entry : map.entrySet()) {
            mapList.add(entry.getKey());
        }
        return mapList;
    }

    public static int verifyTag(Map<String, String> titleMap, Elements trs) {
        // 标签匹配数
        int matchNumber = 0;
        for (Element tr : trs) {
            for (Element element : tr.select("td")) {
                if (element.html().contains("<br>")) {
                    String[] split = element.html().split("<br>\n");
                    if (split.length == 2)
                        if (titleMap.containsKey(split[0]))
                            matchNumber += 1;
                }

                if (titleMap.containsKey(element.text())) {
                    matchNumber += 1;
                }
            }
        }
        return matchNumber;
    }

    // 编译一个正则表达式模式，用于匹配 <br> 标签及其常见变体 (不区分大小写，允许可选斜杠和空白)
    private static final Pattern BR_SPLIT_PATTERN = Pattern.compile("<br\\s*/?>", Pattern.CASE_INSENSITIVE);


    /**
     * 解析给定的 HTML 字符串中的表格，提取还款记录数据。
     * 期望表格格式：第一行是固定头部，后续行为数据。
     * 如果数据单元格包含 <br>，提取 <br> 后的内容；否则提取整个文本。
     *
     * @param table 要解析的 HTML 字符串。
     * @return 包含每行数据的 List of Maps。如果表格不符合预期头部格式，返回空列表。
     */
    public static List<Map<String, String>> parseFlexibleTable(Element table,List<String> expectedHeaders) {
        List<Map<String, String>> result = new ArrayList<>();

        Elements rows = table.select("tr");

        // 至少需要两行：一行头部，至少一行数据
        if (rows.size() < 2) {
            return result;
        }

        // --- 验证并提取头部信息 ---
        Element headerRow = rows.get(0);
        Elements headerTds = headerRow.select("td");
        // 头部列数需要与期望的 Key 数量匹配
        if (headerTds.size() != expectedHeaders.size()) {
            return result; // 头部列数不匹配，格式不符
        }

        // 验证每个头部 td 的文本是否符合预期，并存储实际使用的头部文本作为 Key
        List<String> actualHeaderKeys = new ArrayList<>();
        for (int i = 0; i < headerTds.size(); i++) {
            String headerText = headerTds.get(i).text().trim();
            // 严格检查头部文本是否与期望的头部文本完全匹配
            if (i >= expectedHeaders.size() || !headerText.contains(expectedHeaders.get(i))) {
                break; // 头部文本不匹配，格式不符
            }
            actualHeaderKeys.add(headerText); // 使用实际匹配到的头部文本作为 Key
        }

        // --- 处理数据行 (从索引 1 开始) ---
        for (int i = 1; i < rows.size(); i++) {
            Element dataRow = rows.get(i);
            Elements dataTds = dataRow.select("td");

            // 数据行的列数应该至少和头部列数一样多
            if (dataTds.size() < actualHeaderKeys.size()) {
                continue; // 数据列数不足，跳过这一行
            }

            Map<String, String> rowDataMap = new HashMap<>();

            // 遍历数据行的 td，提取值并与头部 Key 对应
            for (int j = 0; j < actualHeaderKeys.size(); j++) {
                Element dataTd = dataTds.get(j);

                // 提取数据：如果包含 <br> 取后半部分，否则取全部文本
                String extractedValue = extractValueFromDataCell(dataTd);

                // 将提取的值存入 Map，Key 是对应的头部文本
                rowDataMap.put(actualHeaderKeys.get(j), extractedValue);
            }

            result.add(rowDataMap); // 将当前行的数据 Map 添加到结果列表
        }

        return result;
    }


    /**
     * 解析给定的 HTML 字符串中的表格，提取还款记录数据。
     * 期望表格格式：第一行是固定头部，后续行为数据。
     * 如果数据单元格包含 <br>，提取 <br> 后的内容；否则提取整个文本。
     *
     * @param table 要解析的 HTML 字符串。
     * @return 包含每行数据的 List of Maps。如果表格不符合预期头部格式，返回空列表。
     */
    public static List<Map<String, String>> parseBasicFlexibleTable(Element table,List<String> expectedHeaders) {
        List<Map<String, String>> result = new ArrayList<>();

        Elements rows = table.select("tr");

        // --- 验证并提取头部信息 ---
        List<String> actualHeaderKeys = new ArrayList<>();
        List<String>  actualHeaderValues = new ArrayList<>();
        for (Element headerRow : rows){
            Elements headerTds = headerRow.select("td");
            for (int i = 0; i < headerTds.size(); i++) {
                String headerText = headerTds.get(i).text().trim();
                // 严格检查头部文本是否与期望的头部文本完全匹配
                if (expectedHeaders.contains(headerText)) {
                    actualHeaderKeys.add(headerText); // 使用实际匹配到的头部文本作为 Key
                }else {
                    actualHeaderValues.add(headerText);
                }
            }
        }

        // 验证每个头部 td 的文本是否符合预期，并存储实际使用的头部文本作为 Key
        // --- 处理数据行 (从索引 1 开始) ---
        for (int i = 0; i < actualHeaderKeys.size(); i++) {

            Map<String, String> rowDataMap = new HashMap<>();
            if (actualHeaderValues.size() < i) {
                rowDataMap.put(actualHeaderKeys.get(i),"");
            }
            rowDataMap.put(actualHeaderKeys.get(i),actualHeaderValues.get(i));

            result.add(rowDataMap); // 将当前行的数据 Map 添加到结果列表
        }

        return result;
    }

    /**
     * Helper method to extract value from a data cell based on <br> presence.
     * If td.html() contains <br>, returns the part after the first <br>.
     * Otherwise, returns td.text().
     * Both results are trimmed and internal newlines are removed.
     *
     * @param dataCell The Jsoup Element (td) to extract from.
     * @return The extracted and cleaned string value.
     */
    private static String extractValueFromDataCell(Element dataCell) {
        if (dataCell == null) {
            return ""; // Return empty string for null cell
        }

        String cellHtml = dataCell.html();

        // 检查内部 HTML 是否包含 <br> 标签 (或其变体)
        // 使用简单的 contains 检查，如果需要更严格，可以用正则表达式
        // if (cellHtml.contains("<br>") || cellHtml.contains("<BR>")) { // Simple contains
        if (BR_SPLIT_PATTERN.matcher(cellHtml).find()) { // Using regex pattern find()
            // 如果包含 <br>，使用 split 提取 <br> 后面的内容
            String[] parts = BR_SPLIT_PATTERN.split(cellHtml, 2); // split into max 2 parts

            // parts[0] 是 <br> 前的内容
            // parts[1] (如果存在) 是 <br> 后的内容

            String valueAfterBr = "";
            if (parts.length > 1) {
                // 获取 <br> 后的内容，并清理前后的空白及内部换行
                valueAfterBr = parts[1].trim().replaceAll("[\r\n]", "");
            } else if (parts.length == 1 && cellHtml.trim().endsWith("<br>")) {
                // 特殊情况：如果只有一部分，但原始html以<br>结尾，说明<br>后是空的，取空字符串
                // split with limit 2 might give ["content"] for "content<br>", parts[1] would be out of bounds
                // This edge case is handled by checking parts.length > 1
            }
            // 如果 parts.length == 1 且原始html不以<br>结尾，说明没有<br>，这与find()结果矛盾，
            // 理论上find()返回true时，split至少应该得到2个部分 (limit 2) 或 1个部分 if content<br>

            // Jsoup html() 方法获取的内容通常不包含td标签周围的换行
            // 但如果原始HTML有换行，或者<br>后紧跟换行，trim()和replaceAll("[\\r\\n]", "")是必要的

            return valueAfterBr;

        } else {
            // 如果不包含 <br>，提取整个文本内容并清理
            return dataCell.text().trim();
        }
    }

    /**
     * 遍历贷款明细 将非循环贷 循环贷一  循环贷二 贷记卡 4个类型 明细添加到不同的Map中去
     * 获取每个类型的在标签中的起始结束位置，存储中间的标签 用于解析不同类型
     *
     * @param queryDetailsList   用来存储的不同类型table
     * @param startElement 起始标题 Element
     * @param endElement   结束标题 Element
     */
    public static void extractQueryDetailsTablesBetween(Element startElement, Element endElement, List<Element> queryDetailsList) {
        Element currentElement = startElement.nextElementSibling(); // 从起始H2的下一个兄弟元素开始
        String accountName = "";
        // 编辑非循环贷记所有table
        while (currentElement != null && !currentElement.equals(endElement)) {

            // 检查当前元素是否是 table 标签
            if (currentElement.tagName().equalsIgnoreCase("table")) {
                /*
                  将当前table添加到 elementMap 中，并使用 accountName 作为键
                  accountName 是从 h3 标签中提取的，表示当前贷款账户的名称 会重复 prefix 是用来生成唯一键的计数器 来区分
                  elementMap 实现是 LinkedHashMap，所以插入顺序是有序的
                  当按照顺序遍历再次遇到相同的 accountName 表示是同一个账户  accountName 不相同表示下一个账户
                 */
                queryDetailsList.add(currentElement);
            }

            // 移动到下一个兄弟元素
            currentElement = currentElement.nextElementSibling();
        }

    }

    /**
     * 在给定的起始元素和结束元素之间查找 table 元素。
     * 只有当 table 的第一个 tr 中的 td 元素至少包含指定列表中的四个文本时，
     * 才将该 table 添加到 elementMap 中。
     *
     * @param startElement 起始元素 (不包含此元素)
     * @param endElement   结束元素 (不包含此元素)
     * @param elementMap   用于存储符合条件的 table 元素的 Map
     * @param prefix       用于生成 elementMap 键的前缀计数器
     */
    public static void extractLoanTablesBetween(Element startElement,
                                                Element endElement,
                                                Map<String, Element> elementMap,
                                                AtomicInteger prefix,
                                                List<String> targetHeaders) {
        // 从起始元素的下一个兄弟元素开始
        Element currentElement = startElement.nextElementSibling();
        // accountName 在原代码中没有实际提取，这里保留，但其值将是空字符串，prefix用于区分
        AtomicInteger accountName = new AtomicInteger();

        // 遍历起始元素和结束元素之间的所有兄弟元素
        while (currentElement != null && !currentElement.equals(endElement)) {

            // 检查当前元素是否是 table 标签
            if (currentElement.tagName().equalsIgnoreCase("table")) {
                // 调用辅助方法判断当前 table 是否符合贷款表格的特征 如果是table 但又不是不符合特征 则添加上一个表格Map 中 可能是断页了
                if (isLoanTable(currentElement,targetHeaders)) {
                    /*
                      将当前符合条件的 table 添加到 elementMap 中。
                      使用 prefix 生成唯一键，accountName 当前为固定空字符串，实际区分靠 prefix。
                      elementMap 实现是 LinkedHashMap，所以插入顺序是有序的。
                     */
                    elementMap.put(prefix.get() + "_"+accountName.get(), currentElement);
                    prefix.incrementAndGet(); // 每添加一个符合条件的 table，计数器加一
                }else {
                    elementMap.put(prefix.get()-1 + "_"+accountName.get(), currentElement);
                }
            }
            accountName.incrementAndGet();
            // 移动到下一个兄弟元素
            currentElement = currentElement.nextElementSibling();
        }
    }

    /**
     * 判断一个给定的Element（假定是table）是否是符合贷款表格特征的表格。
     * 特征是：td 中至少包含以下字符串中的四个（文本内容完全匹配，忽略首尾空格）：
     * "管理机构", "账户标识", "开立日期", "到期日期", "借款金额", "账户币种"
     *
     * @param tableElement 要检查的 table 元素
     * @return 如果符合特征返回 true，否则返回 false
     */
    private static boolean isLoanTable(Element tableElement,List<String> targetHeaders) {
        // 查找第一个 tr
        Elements trs = tableElement.select("tr");

        // 如果没有 tr 或者第一个 tr 为空，则不是符合的表格
        if (trs.isEmpty()) {
            return false;
        }
        int matchNumber = targetHeaders.size();
        // 查找 tr 下的所有 td
        AtomicInteger matchCount = new AtomicInteger(0);
        for (Element firstTr : trs) {

            Elements tds = firstTr.getElementsByTag("td");


            // 遍历所有的 td，检查其文本内容
            for (Element td : tds) {
                // 获取并清理 td 的文本内容（移除首尾空格）
                String tdText = td.text().trim();

                // 检查 td 的文本是否在目标表头列表中
                // 注意：这里是精确匹配，不区分大小写可以使用 containsIgnoreCase 或转换为小写比较
                // 根据需求，目前是精确匹配（区分大小写），除非"账户币种中的4个"是指文本内容完全一致
                if (targetHeaders.contains(tdText)) {
                    matchCount.incrementAndGet();
                }
            }
        }
        // 如果匹配数已经达到或超过4个，说明是目标表格，可以直接返回 true，提高效率
        return matchCount.get() >= matchNumber;
    }



    // 定义表格格式枚举
    public enum TableFormat {
        FORMAT1,    // 格式 1: 表头是 "年份", 后面是数字月份，数据 td 中可能有 <br>
        FORMAT2,    // 格式 2: 第一行是年份，后面是数据 td 中包含 <br>，没有明确的数字月份 header
        FORMAT3,    // 格式 3: 表头是空 + 数字月份，后面是两行数据 (年份行 + 数据行)
        UNKNOWN     // 未知格式
    }

    // 用于检查四位数字年份的正则表达式
    public static final Pattern YEAR_PATTERN = Pattern.compile("^\\s*\\d{4}\\s*$"); // 匹配以4位数字为主的内容，允许前后有空白

    // 用于检查是否包含 <br> 的正则表达式 (不区分大小写，允许可选斜杠和空白)
    // 匹配 <br> 及其变体 (不区分大小写)
    private static final Pattern FLEXIBLE_BR_PATTERN = Pattern.compile("<br\\s*/?>", Pattern.CASE_INSENSITIVE);


    // 假设你有一个 isPositiveInteger 方法
    private static boolean isPositiveInteger(String s) {
        if (s == null || s.trim().isEmpty()) {
            return false;
        }
        try {
            int i = Integer.parseInt(s.trim());
            return i > 0; // 正整数
        } catch (NumberFormatException e) {
            return false;
        }
    }


    // 定义 FORMAT1 第二行月份数据允许的值集合
    private static final Pattern TRIGGER_CHAR_PATTERN = Pattern.compile("[*NCBMDZ]");
    // 数字是否是1-12之间
    private static final Pattern MONTH_PATTERN = Pattern.compile("^(1[0-2]|[1-9])$");

    /**
     * 判断给定的 HTML 字符串中是否包含一个匹配还款记录 Table 三种格式之一的表格。
     * 按顺序尝试匹配 FORMAT1, FORMAT3, FORMAT2。
     *
     * @param html 要判断的 HTML 字符串。
     * @return 匹配到的 TableFormat；如果未匹配任何格式，返回 UNKNOWN。
     */
    public static TableFormat determineTableFormat(String html) {
        if (html == null || html.trim().isEmpty()) {
            return TableFormat.UNKNOWN;
        }

        Document doc = Jsoup.parse(html); // 解析整个 HTML 字符串
        Elements tables = doc.select("table"); // 选取所有的 table 元素

        // 遍历找到的所有表格
        for (Element table : tables) {
            Elements rows = table.select("tr");

            // 至少需要一行来判断格式
            if (rows.isEmpty()) {
                continue;
            }

            // --- 尝试匹配 FORMAT2 (Fallback, 数据 td 中包含 <br>) ---
            // FORMAT2 的数据行通常以年份开头，且数据 td 中包含 <br>
            // 我们检查第一行（如果第一行中包含<br> 则为 FORMAT2

            int rowsToCheckForFormat2 = Math.min(rows.size(), 1);// 只检查1行作为代表
            for (int i = 0; i < rowsToCheckForFormat2; i++) {
                boolean headerNumbersMatch = false;

                Elements dataTds = rows.get(i).select("td");
                // 确保行有足够的列 (至少 13 个: 年份 + 12个月) 并且第一个 td 看起来像年份
                if (dataTds.size() >= 12) {
                    // 检查数据 td (索引 1 到 12)
                    for (int j = 1; j < 12; j++) {
                        if (j < dataTds.size()) { // 防止索引越界
                            String tdHtml = dataTds.get(j).html(); // 获取包含 <br> 的内部 HTML
                            if (FLEXIBLE_BR_PATTERN.matcher(tdHtml).find()) {
                                // 使用正则表达式检查是否包含 <br>
                                headerNumbersMatch = true;
                                break; // 匹配到 <br> 就退出检查
                            }
                        }
                    }
                }
                if (headerNumbersMatch) {
                    return TableFormat.FORMAT3; // 匹配 FORMAT2
                }
            }

            // --- 尝试匹配 FORMAT1 和 FORMAT1 (年份 header) ---
            // FORMAT1 需要至少 3 行 (header + 数据)
            // 特征: 第一行第一个 td 是 月份，且第一行有 >= 12 个 td (12个月数字)
            // 附加检查: 第二行（第一个数据行）的第一个 td 看起来像一个年份 (四位数字) 并且后面可能是 "*","N", "C", "B", "M", "D", "Z","#" ""；
            // 满足上诉条件 第二行包含<br> 则匹配 FORMAT2  不包含 则匹配 FORMAT1
            if (rows.size() >= 2) {
                Elements firstRowTds = rows.get(0).select("td");
                Elements secondRowTds = rows.get(1).select("td"); // 第二行是第一行数据
                /*
                    第一行第一个 td 是 月份，且第一行有 >= 12 个 td (12个月数字)
                 */
                // 检查后续 12 个 td 是否是数字 1 到 12
                AtomicInteger headerNumbersMatch = new AtomicInteger(0);
                for (int j = 1; j < 13; j++) {
                    // 防止索引越界，并检查文本是否是数字 j 的字符串形式
                    if (firstRowTds.size() - 1 >= j) {
                        String tdValue = firstRowTds.get(j).text().trim();
                        // 不为null 且不为空  且不是纯数字
                        if (StringUtils.isNotBlank(tdValue) && !MONTH_PATTERN.matcher(tdValue).find()) {
                            headerNumbersMatch.set(1);
                            break; // 不符合就退出检查
                        }
                    }
                }
                /*
                    第二行（第一个数据行）的第一个 td 看起来像一个年份 (四位数字) 并且后面可能是 "*","N", "C", "B", "M", "D", "Z","#" ”“；
                 */
                if (headerNumbersMatch.get() != 1) {
                    // 检查第二行 (数据行) 的基本结构和第一个 td (年份)
                    Element element = secondRowTds.get(0);
                    String year = element.text().trim();
                    // 如果只有二行 第二行可能没有年份 那第一行的年份
                    if (!StringUtils.isNotBlank(year)) {
                        year = firstRowTds.get(0).text().trim();
                    }
                    if (secondRowTds.size() < 13 || YEAR_PATTERN.matcher(year).matches()) {
                        // 检查后续 12 个 td (数据) 是否在允许的字符集合中
                        for (int j = 1; j < 13; j++) {
                            Element trimmed =  null;
                            if (firstRowTds.size() - 1 >= j) {
                                try {
                                     trimmed = secondRowTds.get(j);
                                }catch (IndexOutOfBoundsException index){
                                    break;
                                }
                                // 长度小于12  或者 字段不包含 "*","N", "C", "B", "M", "D", "Z","#" ”“  或者 包含<br>
                                if (StringUtils.isNotBlank(trimmed.text().trim()) && TRIGGER_CHAR_PATTERN.matcher((trimmed.text().trim())).find()) {
                                    if (trimmed.html().contains("<br>")) {
                                        headerNumbersMatch.set(2);
                                        break;
                                    } else {
                                        headerNumbersMatch.set(3);
                                    }
                                }
                            }
                        }
                        /*
                          如果有行数 有年份 单身判断不到 可能所有值都是空
                          判断第三行是否有年份 如果有 则匹配 FORMAT2  否则匹配 FORMAT1
                         */
                        if (rows.size() >= 3 && headerNumbersMatch.get() == 0) {
                            Elements threeRowTds = rows.get(2).select("td");
                            Element threeElement = threeRowTds.get(0);
                            String threeYear = threeElement.text().trim();
                            // 如果只有二行 第二行可能没有年份 那第一行的年份
                            if (StringUtils.isNotBlank(threeYear)) {
                                return TableFormat.FORMAT2; // 匹配 FORMAT2
                            } else {
                                return TableFormat.FORMAT1; // 匹配 FORMAT1
                            }
                        } else {
                            return TableFormat.FORMAT2; // 匹配 FORMAT2
                        }
                    }

                }
                if (headerNumbersMatch.get() == 3) {
                    return TableFormat.FORMAT1; // 匹配 FORMAT1
                } else if (headerNumbersMatch.get() == 2) {
                    return TableFormat.FORMAT2; // 匹配 FORMAT2
                }
            }


        }
        // 如果遍历完所有表格都没有匹配到任何格式
        return TableFormat.UNKNOWN;
    }


   // 正则表达式：匹配最常见的中文汉字范围 (Unicode CJK Unified Ideographs)

    private static final Pattern CHINESE_CHAR_PATTERN = Pattern.compile("[\u4E00-\u9FFF]");

    /**
     * 检查给定的 Jsoup Element (通常是 tr) 是否包含中文汉字。
     * 这个方法会获取 Element 及其所有子元素的文本内容，并检查是否存在中文汉字。
     *
     * @param elementToCheck 要检查的 Jsoup Element (如 tr)。
     * @return 如果 Element 及其子元素的文本内容包含中文汉字，返回 true；否则返回 false。
     */
    public static boolean containsChinese(Element elementToCheck) {
        if (elementToCheck == null) {
            return false;
        }

        // 获取 Element 及其所有子元素的纯文本内容
        String text = elementToCheck.text();

        // 如果文本为空，则不包含中文
        if (text == null || text.isEmpty()) {
            return false;
        }

        // 使用正则表达式检查文本中是否存在中文汉字
        Matcher matcher = CHINESE_CHAR_PATTERN.matcher(text);

        // find() 方法会在输入序列中查找与模式匹配的下一个子序列。
        // 只要找到一个中文汉字，就返回 true。
        return matcher.find();
    }

    /**
     * 验证给定的表格 Element 中的所有 tr 元素是否都不包含中文汉字。
     * 如果任一 tr 包含中文汉字，则返回 false。
     * 这个方法更接近你原始的需求，它验证的是表格的 tr 是否符合要求。
     *
     * @param trElement 要验证的 Jsoup tr Element。
     * @return 如果所有 tr 都不包含中文汉字，返回 true；否则返回 false。
     */
    public static boolean verifyTrsContainNoChinese(Element trElement) {
        // 对 null 输入进行基本检查
        if (trElement == null) {
            // log.info("Input table element is null."); // 可选的调试输出
            return false;
        }

        // 选择所有 td 元素
        Elements tds = trElement.select("td");

        // 遍历每个 tr
        for (Element td : tds) {
            // 检查当前 tr 是否包含中文汉字
            if (containsChinese(td)) {
                // log.info("Found Chinese character in tr: '" + tr.text() + "'"); // 可选的调试输出
                return false; // 如果任一 tr 包含中文，立即返回 false
            }
        }

        // 如果遍历完所有 td 都没有找到中文汉字
        // log.info("No Chinese characters found in any tr."); // 可选的调试输出
        return true;
    }


    // 定义允许的 td 文本值集合
    private static final Set<String> ALLOWED_TD_VALUES = new HashSet<>(Arrays.asList(
            "编号", "查询日期", "查询机构", "查询原因"
    ));

    /**
     * 判断给定的 tr 元素中的 td 值是否都属于允许的集合。
     * 如果任一 td 的文本不是允许集合中的值，则返回 true。
     * 如果所有 td 的文本都在允许集合中，则返回 false。
     *
     * @param trElement 要检查的 Jsoup tr Element。
     * @return 如果任一 td 的文本不是允许的值，返回 true；否则返回 false。
     */
    public static boolean hasNonAllowedTdValue(Element trElement) {
        // 对 null 输入进行基本检查
        if (trElement == null) {
            // null 没有 td，所以不会有非允许的值，返回 false
            return false;
        }
        // 确保输入的是 tr 标签 (虽然方法名暗示了，但明确检查更安全)
        if (!trElement.tagName().equalsIgnoreCase("tr")) {
            // 不是 tr 标签，不包含 td，所以不会有非允许的值，返回 false
            return false;
        }

        // 选择所有 td 元素
        Elements tds = trElement.select("td");

        // 遍历每个 td
        for (Element td : tds) {
            // 获取 td 的文本内容并去除前后空白
            String tdText = td.text().trim();

            // 检查当前 td 的文本是否 不 包含在允许的值集合中
            if (!ALLOWED_TD_VALUES.contains(tdText)) {
                // 如果找到任何一个 td 的文本不在允许集合中，立即返回 true
                return true;
            }
        }

        // 如果循环完成，说明所有 td 的文本都在允许的值集合中
        return false;
    }

    /**
     * 将多个 Jsoup 表格 Element 的行合并到列表中的第一个表格。
     * 行将从源表格移动到目标表格的 tbody 中。
     * 如果目标表格没有 tbody，则会创建一个，并将目标表格中已有的行移动到这个新的 tbody 中。
     *
     * @param tables 代表表格的 Jsoup Element 对象列表。
     * @return 列表中第一个 Element（现在包含所有合并的行），如果列表为 null 或为空则返回 null。
     */
    public static Element mergeTables(List<Element> tables) {
        // 处理列表为空或 null 的情况
        if (tables == null || tables.isEmpty()) {
            log.info("输入表格列表为空或null，无法合并。");
            return null;
        }

        // 如果只有一个表格，无需合并，直接返回第一个表格
        if (tables.size() == 1) {
            log.info("输入表格列表只有一个表格，直接返回。");
            return tables.get(0);
        }

        // 使用列表中的第一个表格作为目标表格
        Element targetTable = tables.get(0);
        log.info("以第一个表格作为目标表格进行合并。");

        // 获取或创建目标表格的 tbody 元素
        // 所有行都将追加到这个 tbody 中，使得结构更规范
        Element targetTbody = targetTable.selectFirst("tbody");

        if (targetTbody == null) {
            log.info("目标表格没有 tbody，正在创建一个并将现有行移动进去。");
            // 如果目标表格没有 tbody，则创建一个新的 tbody 元素
            targetTbody = new Element("tbody"); // 先创建，暂不附加到 DOM 树

            // 将目标表格中原有的所有 tr 元素（包括 thead/tfoot/direct trs）移动到新的 tbody 中
            Elements existingTargetRows = targetTable.select("tr");
            for (Element existingRow : existingTargetRows) {
                // appendChild 会自动将元素从原位置移除，然后添加到新位置 (这里是新的 targetTbody)
                targetTbody.appendChild(existingRow);
            }

            // 将包含原有行的新 tbody 添加到目标表格中
            // 上面的循环已经将旧行移入新 tbody。现在只需将新 tbody 加入 targetTable。
            targetTable.appendChild(targetTbody);
           log.info("已创建 tbody 并移动了目标表格原有行。");

        } else {
            log.info("目标表格已包含 tbody。");
            // 如果目标表格已经有 tbody，新行将追加到现有行后面。
            // targetTbody.empty(); // 如果需要清空目标表格原有的行，取消此行注释
        }


        // 遍历列表中的其他表格（从第二个开始）
        for (int i = 1; i < tables.size(); i++) {
            Element sourceTable = tables.get(i);
            log.info("正在从源表格 " + (i+1) + " 合并行...");

            // 选中当前源表格中的所有 tr 元素
            // select("tr") 会找到表格内的所有 tr，无论它们是否在 thead, tbody, tfoot 中，或直接作为 table 的子元素
            Elements rowsFromSource = sourceTable.select("tr");
            log.info("找到 " + rowsFromSource.size() + " 行。");

            // 将选中的行追加到目标表格的 tbody 中
            for (Element row : rowsFromSource) {
                // appendChild 方法会自动将行从源表格中移除，然后添加到目标表格的 tbody 中
                targetTbody.appendChild(row);
            }

            // 可选：如果不再需要原始的源表格元素，可以将其从 DOM 树中移除
            // sourceTable.remove(); // 根据你的具体需求决定是否需要这行
            log.info("已将源表格 " + (i+1) + " 的行添加到目标表格。");
        }

        log.info("所有表格行已合并完成。");

        // 返回合并后的目标表格
        return targetTable;
    }

    /**
     * 检查一组 <td> 元素，统计其中包含数字（范围 1-12）的个数，
     * 并处理特殊的数字提取格式（如 <td>1<br>*<br>2</td> 只取第一个数字）。
     *
     * @param tdElements Jsoup 的 Elements 集合，代表需要检查的 <td> 标签。
     * @return 如果有 10 个或更多 <td> 元素符合条件（包含 1-12 之间的数字，基于指定的提取逻辑），则返回 true，否则返回 false。
     */
    public static boolean checkTableCells(Elements tdElements) {
        int matchCount = 0; // 匹配计数器
        final int requiredMatches = 10; // 需要达到的匹配次数
        final int rangeMin = 1; // 数字范围最小值
        final int rangeMax = 12; // 数字范围最大值

        // 如果元素集合为空或少于需要的匹配次数，直接返回 false
        if (tdElements == null || tdElements.size() < requiredMatches) {
            // 如果元素数量少于 10 个，则不可能匹配成功 10 次。
            return false;
        }

        // 遍历每个 <td> 元素
        for (Element td : tdElements) {
            // 使用定义的规则提取数字
            Integer number = extractNumber(td);

            // 检查是否成功提取到数字，并且数字在 1-12 的范围内
            if (number != null && number >= rangeMin && number <= rangeMax) {
                matchCount++; // 匹配计数加一
            }

            // 可选：如果已经达到足够的匹配次数，提前退出循环
            if (matchCount >= requiredMatches) {
                return true;
            }
        }

        // 检查完所有元素后，看是否达到要求的匹配次数
        return matchCount >= requiredMatches;
    }

    /**
     * 从 <td> 元素的内容中提取可能的整数。
     * 处理特殊情况 <td>1<br>*<br>2</td>，提取第一个 <br> 之前的第一个数字。
     * 如果特殊情况不适用或第一部分不是数字，则尝试解析元素的完整文本内容。
     *
     * @param td 代表 <td> 标签的 Jsoup Element 对象。
     * @return 提取到的整数；如果根据规则无法解析出有效的数字，则返回 null。
     */
    private static Integer extractNumber(Element td) {
        if (td == null) {
            return null;
        }

        // 获取元素的内部 HTML，保留 <br> 标签以便后续处理
        String innerHtml = td.html();

        // 规则 1：检查特殊格式，例如 "数字<br>..."
        // 判断是否包含 <br> 标签 (允许 <br/>, <br > 等格式)
        if (innerHtml.contains("<br")) {
            // 按 <br> 分割 (允许 <br/>, <br > 等格式，并忽略周围的空白)
            String[] parts = innerHtml.split("<br\\s*/?>");
            if (parts.length > 0) {
                // 获取第一部分内容（HTML 字符串格式），并去除首尾空白
                String firstPartHtml = parts[0].trim();

                // 解析第一部分的 HTML，获取其纯文本内容并去除首尾空白
                // Jsoup.parseBodyFragment().text() 可以去除 HTML 标签（如 <b>）
                String cleanedFirstPartText = Jsoup.parseBodyFragment(firstPartHtml).text().trim();

                // 如果清理后的文本不为空
                if (!cleanedFirstPartText.isEmpty()) {
                    try {
                        // 尝试将清理后的第一部分文本解析为整数
                        return Integer.valueOf(cleanedFirstPartText);
                    } catch (NumberFormatException e) {
                        // 如果这部分不是一个简单的数字，则捕获异常，继续执行下面的规则
                        // System.out.println("Debug: First part '" + firstPartHtml + "' cleaned to '" + cleanedFirstPartText + "' is not a number.");
                    }
                }
            }
        }

        // 规则 2：如果特殊格式不适用或未能提取到数字，
        // 尝试解析 <td> 的完整文本内容。
        // Jsoup 的 text() 方法会连接文本节点，处理 <br> 时通常会添加空格。
        String fullText = td.text().trim();
        if (!fullText.isEmpty()) {
            try {
                // 尝试将完整文本解析为整数
                return Integer.valueOf(fullText);
            } catch (NumberFormatException e) {
                // 如果完整文本不是一个简单的数字，则捕获异常
                // System.out.println("Debug: Full text '" + fullText + "' is not a number.");
            }
        }

        // 如果两种规则都未能成功提取并解析出数字
        return null;
    }

    public static void main(String[] args) {


    }

}
